package org.ravey.plugin;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class PluginManifestManager {
	
	Document doc;
	DocumentBuilder docbuild;
	String ApplicationName;
	
	/**
	 * Constructor for empty PluginManifestManager objects.
	 * 
	 * Create an empty PluginManifestManager that can be manipulated to make a manifest which
	 * can then be saved. This is used by applications to decide which plugins to activate etc. 
	 * 
	 * @param AppName The Application's name e.g. "My Application"
	 */
	public PluginManifestManager(String AppName){
		
		//set the internal value for the application name
		ApplicationName = AppName;
		
		//create a new document builder factory
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		
		try {
			//create a new document builder
			docbuild = dbf.newDocumentBuilder();
			
			//build an empty document
			doc = docbuild.newDocument();
			
			//create a root element for the document
			Element root = doc.createElement("PluginManifest");
			//set the application title to the given appname argument
			root.setAttribute("ApplicationTitle", AppName);
			//add the root element to the document
			doc.appendChild(root);
			
			
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	public PluginManifestManager(String AppName, File ManifestFile){
		
		//set the internal value for the application name
		ApplicationName = AppName;
		
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		try {
			docbuild = dbf.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		LoadManifest(ManifestFile);
		
	}
	
	/**
	 * Add a plugin to the manifest file
	 * @param plugin Plugin information representing the plugin to be added to the manifest
	 * @return
	 */
	public boolean AddPlugin(PluginInfo plugin){
		
		//make sure that this plugin isn't already in the manifest
		try {
			if(GetPluginInfo(plugin.GetID()) != null)
				return false;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//create a new plugin element
		Element pluginEl = doc.createElement("plugin");
		
		//get the root document of the document
		Element root = (Element)doc.getFirstChild();
		
		//set the plugin data
		pluginEl.setAttribute("uuid", plugin.GetID());
		pluginEl.setAttribute("title", plugin.GetTitle());
		pluginEl.setAttribute("author", plugin.GetAuthor());
		pluginEl.setAttribute("mainClass", plugin.GetMainClass());
		pluginEl.setAttribute("JarFilePath", plugin.GetJarPath());
		pluginEl.setAttribute("MinimumKernel", String.valueOf(plugin.GetKernelVersion()));
		
		//add the new plugin to the root element.
		root.appendChild(pluginEl);
		
		return true;
	}
	
	/**
	 * If plugin exists, remove it from the document
	 * @return
	 * @throws Exception 
	 */
	public boolean RemovePlugin(PluginInfo plugin) throws Exception{
		
		//get a list of all of the plugin nodes
		NodeList pluginNodes = doc.getElementsByTagName("plugin");
		
		//if there aren't any nodes, throw an error.
		if(pluginNodes.getLength() < 1)
			throw new Exception("There must be at least one plugin to test in the manifest");
		
		//check each plugin to see if its the one we want.
		for(int i=0;i<pluginNodes.getLength();i++){
			Element pluginEl = (Element)pluginNodes.item(i);
			if(pluginEl.getAttribute("uuid") == plugin.GetID()){
				//try to remove the element
				doc.removeChild(pluginEl);
				//return success to the caller
				return true;
			}
		}
		//the element couldn't be found, return false to the caller
		return false;

	}
	
	/**
	 * Toggles a plugin's status: enabled or disabled. 
	 * @param plugin the PluginInfo for the plugin you want to manipulate
	 * @param disabled if true then disable plugin, if false then enable plugin.
	 * @return
	 * @throws Exception
	 */
	public boolean SetPluginDisabled(PluginInfo plugin, boolean disabled) throws Exception{
		
		//get a list of all of the plugin nodes
		NodeList pluginNodes = doc.getElementsByTagName("plugin");
		
		//if there aren't any nodes, throw an error.
		if(pluginNodes.getLength() < 1)
			throw new Exception("There must be at least one plugin to test in the manifest");
		
		//check each plugin to see if its the one we want.
		for(int i=0;i<pluginNodes.getLength();i++){
			Element pluginEl = (Element)pluginNodes.item(i);
			if(pluginEl.getAttribute("uuid") == plugin.GetID()){
				//try to set the disabled status to either true or false
				if(disabled == true)
					pluginEl.setAttribute("status", "disabled");
				else
					pluginEl.setAttribute("status", "enabled");
				//return success to the caller
				return true;
			}
		}
		//the element couldn't be found, return false to the caller
		return false;
	}
	
	/**
	 * See if there is an entry in the manifest for a given UUID.
	 * @param UUID
	 * @return
	 * @throws Exception 
	 */
	public PluginInfo GetPluginInfo(String UUID) throws Exception{
		
		NodeList pluginNodes = doc.getElementsByTagName("plugin");
		
		if(pluginNodes.getLength() < 1)
			throw new Exception("There must be at least one plugin to test in the manifest");
		
		for(int i=0;i<pluginNodes.getLength();i++){
			Element pluginEl = (Element)pluginNodes.item(i);
			if(pluginEl.getAttribute("uuid") == UUID){
				return new PluginInfo(
						UUID,
						pluginEl.getAttribute("title"),
						pluginEl.getAttribute("author"),
						pluginEl.getAttribute("mainClass"),
						pluginEl.getAttribute("JarFilePath"),
						Integer.parseInt(pluginEl.getAttribute("MinimumKernel"))
						);
			}
		}
		
		return null;

		
	}
	
	/**
	 * Load a saved manifest from a file and parse it
	 * @param ManifestFile
	 */
	public void LoadManifest(File ManifestFile){
		
		try {
			doc = docbuild.parse(ManifestFile);
			
			Element root = (Element)doc.getFirstChild();
			
			if(!root.getAttribute("ApplicationTitle").equalsIgnoreCase(ApplicationName))
			{
				throw new Exception("Cannot load a manifest for another program.");
			}
			
		} catch (SAXException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Save the manifest file. Warning, this function performs blind overwrites
	 * 
	 * Note: This is a convenience alias for SaveManifest(File ManifestFile)
	 * 
	 * @param ManifestFilePath the path of the manifest file.
	 * @return true if the operation was a success, false if an error occurs.
	 */
	public boolean SaveManifest(String ManifestFilePath){
		return SaveManifest(new File(ManifestFilePath));
	}
	
	/**
	 * Save the manifest file. Warning, this function performs blind overwrites
	 * 
	 * @param ManifestFile a file object representing the manifest file.
	 * @return true if the operation was a success, false if an error occurs.
	 */
	public boolean SaveManifest(File ManifestFile){
		
		if(!ManifestFile.exists())
		{
			try {
				ManifestFile.createNewFile();
			} catch (IOException e) {
				return false;
			}
		}
		
		//get an output write stream for the document
		FileOutputStream fout;
		try {
			fout = new FileOutputStream(ManifestFile);

			//create a serializer object
			XMLSerializer s = new XMLSerializer();
			//set the serializer to the provided output stream
			s.setOutputByteStream(fout);
			
			//serialize the document
			s.serialize(doc);
			
			//close the stream
			fout.close();
			//if all of the above worked, return true.
			return true;
		} catch (FileNotFoundException e) {
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return false;
	}
	
	
	/**
	 * Return a list of PluginInfo Elements to the caller
	 * 
	 * Returns a list of PluginInfo objects. If the boolean argument is true then all of the
	 * plugins in the manifest are returned. If the boolean argument is false then only plugins
	 * that have "enabled" status will be loaded and sent to the caller.
	 * 
	 * @return List of active plugins
	 * @throws Exception 
	 */
	public ArrayList<PluginInfo> GetPluginInfoList(boolean ShowDisabled) throws Exception{
		NodeList pluginNodes = doc.getElementsByTagName("plugin");
		ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>();
		
		
		if(pluginNodes.getLength() < 1)
			return null;

		 //Load information about all plugins and 
		for(int i=0;i<pluginNodes.getLength();i++){
			
			//get the plugin element from the list of nodes
			Element pluginEl =  (Element)pluginNodes.item(i);
			
			//if the plugin is disabled, skip it.
			if(pluginEl.getAttribute("status") == "disabled" && ShowDisabled == false)
				continue;
			String uuid = pluginEl.getAttribute("uuid");
			String title = pluginEl.getAttribute("title");
			String author = pluginEl.getAttribute("author");
			String MainClass = pluginEl.getAttribute("mainClass");
			String JarFile = pluginEl.getAttribute("JarFilePath");
			int KernelVersion = Integer.parseInt(pluginEl.getAttribute("MinimumKernel"));
			plugins.add(new PluginInfo(uuid, title, author, MainClass, JarFile, KernelVersion));
		}
		
		//now that we've collected lots of info in our plugin list, return to caller.
		return plugins;
		
	}
}
